<?php
/**
 * Assembler
 *
 *     Based on Scmwiki parser - Copyright (C) 2010-2011
 *     Callan Bryant <callan1990@googlemail.com>
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Callan Bryant <callan1990@googlemail.com>
 */

class assembler
{
	// number of HTTP requests saved
	//public $requests_saved = 0;

	// working copy of input file (to be converted)
	protected $copy;

	// etags of files loaded, to allow checking for 'current' state
	// map: [filepath] => etag
	//protected $etags;

	/**
 	 * @param string $filepath file to parse
	 */
	public function __construct($filepath)
	{
		echo "Assembling $filepath...\n";

		$this->copy = assembler::load_file($filepath);

		// change into the source directory so that relative links work
		chdir ( dirname($filepath) );

		// update the copy
		$this->embed_includes();
		$this->embed_javascript();
		$this->embed_CSS();
		// media must be last - looks for generic src's
		$this->embed_binary();

		// breaks some JS...?
		// remove any excessive whitespace (agressive)
		//$this->copy = preg_replace('/(\s)\s+/','$1',$this->copy);
	}

	protected function embed_includes()
	{
		// first group must match whole block
		$this->replace_callback('/<!\-\-#include virtual="(.+)" \-\->/Ui','assembler::load_file');
	}

	protected function embed_javascript()
	{
		// first group must match whole block
		$this->replace_callback('/<script.+src="(?!http)(.+)".*\>.*<\/script>/Ui',function($filepath)
		{
			$script = assembler::load_file($filepath);

			if (MINIFY_JS)
			{
				echo "Minifying $filepath\n";
				$script = JSMin::minify($script);
			}
			else
				echo "Including $filepath\n";

			return '<script>'.$script.'</script>';
		});
	}

	protected function embed_CSS()
	{
		// first group must match whole block
		$this->replace_callback('/<link.+href="(.+)".*\/?>/Ui',function($filepath)
		{
			// parse CSS too with this parser first			
			$css = new assembler($filepath);

			if (MINIFY_CSS)
			{
				echo "Minifying $filepath\n";
				$css = Minify_CSS_Compressor::process($css);
			}
			else
				echo "Including $filepath\n";

			return '<style>'.$css.'</style>';
		});
	}

	// replaces any image/font URLs (etc) to image base64 encoded URIs
	// that are not matched to anything previous
	protected function embed_binary()
	{
		// media in HTML
		$this->replace_callback('/src="(?!http:|data:)(.+)"/Ui',function($filepath)
		{
			echo "Encoding $filepath\n";
			return 'src="'.assembler::encode_data_uri($filepath).'"';
		});

		// media in CSS
		$this->replace_callback('/url\(["\'](?!http:|data:)(.+)["\']\)/Ui',function($filepath)
		{
			echo "Encoding $filepath\n";
			return "url('".assembler::encode_data_uri($filepath)."')";
		});
	}

	public static function encode_data_uri($filepath)
	{
		$extension = http_response::extension($filepath);
		$mimetype = http_response::mimetype($extension);
		$contents = assembler::load_file($filepath);
		return 'data:'.$mimetype.';base64,'.base64_encode($contents);
	}

	/**
	 * Wrapper: searches the working copy (article) for a regex and replaces
	 * the full match with a callback, given the FIRST GROUP!
	 * 
	 * The given callback must accept a matched (first group!) string as
	 * its only parameter, and return the replacement.
	 * 
	 * It is recommended that the callback is to be a anonymous function;
	 * as it will almost certainly be to bespoke to be re-usable.
	 * 
	 * @param string $regex to search for
	 * @param callback $callback
	 */
	public function replace_callback($regex,$callback)
	{
		// get an array of FULL PATTERN matches
		$matches = array();
		preg_match_all($regex,$this->copy,$matches,PREG_PATTERN_ORDER);
		$matches = $matches[0];
		
		foreach ($matches as &$match)
		{
			// find first group of match
			$first = self::match_once($regex,$match);

			// replace matches with the output of its respective callback,
			// giving it the first group as the parameter
			$this->copy = str_replace($match,call_user_func($callback,$first),$this->copy);
		}
	}

	/**
	 * simple wrapper for preg_match
	 * returns the first thing that matches the first backreference
	 * @param string $regex regex, with one backreference, to matches
	 * @param string &$haystack
	 * @return string first match
	 */
	public static function match_once($regex,&$haystack)
	{
		$matches = array();
		preg_match($regex,$haystack,$matches);
		
		// return the first backreference (index 0 is the entire expression)
		if (isset($matches[1]))		
			return $matches[1];
		else
			return false;
	}

	/**
	 * MAGIC: returns the actual article. Metadata is accessed by attributes
	 *
	 */
	public function __toString()
	{
		return $this->copy;
	}

	// loads a file, Exceptions on errors
	public static function load_file($filepath)
	{
		if(!file_exists($filepath))
			throw new Exception("$filepath does not exist");

		// load an up to date copy of the source
		$file = @file_get_contents($filepath);

		if ($file === false)
			throw new Exception("could not load $filepath - check permissions");

		return $file;

	}
}
?>
